package net.homelinux.chaoswg.io.renga.core.gui.operations;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

import javax.swing.JOptionPane;

import net.homelinux.chaoswg.io.datatypes.Pair;
import net.homelinux.chaoswg.io.datatypes.Quad;
import net.homelinux.chaoswg.io.datatypes.Quint;
import net.homelinux.chaoswg.io.datatypes.Triplet;
import net.homelinux.chaoswg.io.datatypes.Tupel;
import net.homelinux.chaoswg.io.datatypes.Unit;
import net.homelinux.chaoswg.io.higherorderfunctions.BinaryFunction;
import net.homelinux.chaoswg.io.higherorderfunctions.ConstantFunction;
import net.homelinux.chaoswg.io.higherorderfunctions.NAryFunction;
import net.homelinux.chaoswg.io.higherorderfunctions.QuadtaryFunction;
import net.homelinux.chaoswg.io.higherorderfunctions.QuintaryFunction;
import net.homelinux.chaoswg.io.higherorderfunctions.TernaryFunction;
import net.homelinux.chaoswg.io.higherorderfunctions.UnaryFunction;
import net.homelinux.chaoswg.io.renga.core.animations.Animation;
import net.homelinux.chaoswg.io.renga.core.converter.Converter;
import net.homelinux.chaoswg.io.renga.core.converter.IdentityConverter;
import net.homelinux.chaoswg.io.renga.core.gui.DrawingArea;
import net.homelinux.chaoswg.io.renga.core.gui.operations.helper.CantGetResultException;
import net.homelinux.chaoswg.io.renga.core.gui.operations.helper.Connection;
import net.homelinux.chaoswg.io.renga.core.gui.operations.helper.ConversionException;
import net.homelinux.chaoswg.io.renga.core.gui.operations.visualisations.Visualisation;
import net.homelinux.chaoswg.io.renga.core.helper.datatypes.Octet;
import net.homelinux.chaoswg.io.renga.core.helper.functions.OctetaryFunction;
import net.homelinux.chaoswg.io.renga.core.registries.ConverterRegistry;
import net.homelinux.chaoswg.io.renga.core.registries.GlobalPropertyRegistry;

import org.jdom.Element;

//Facade pattern
public class PrimitiveOperation extends Operation {
    final protected NAryFunction function;
    final protected Animation animation;

    final protected Class [] inputTypes;
    final protected GlobalPropertyRegistry gpr = GlobalPropertyRegistry.getGlobalPropertyRegistry();
    
    private boolean doneOnce = false;

    public PrimitiveOperation (final NAryFunction function, final Animation animation, final Visualisation visualisation) {
        this.function = function;
        this.animation = animation;
        this.visualisation = visualisation;

        inputs = calculateNumberOfInputs(function);
        outputs = calculateNumberOfOutputs(function);

        visualisation.setInputs(inputs);
        visualisation.setOutputs(outputs);

        inputTypes = calculateInputTypes(this.function);

        connections = new Connection [inputs];

        for (int i = 0; i < inputs; ++i) {
            connections[i] = new Connection();
        }
    }

    private Class [] calculateInputTypes (final NAryFunction f) {
        final Class [] toReturn = new Class [inputs];

        for (final Type t0 : this.function.getClass().getGenericInterfaces()) {
            if (t0 instanceof ParameterizedType) {
                final ParameterizedType t1 = ((ParameterizedType)t0);
                final Type t2 = t1.getRawType();

                if (t2 instanceof Class) {
                    if (NAryFunction.class.isAssignableFrom( ((Class)t2) )) {
                        Type types [] = t1.getActualTypeArguments();
                        for (int i = 0; i < inputs; ++i) {
                            toReturn[i] = ((Class)types[i+1]);
                        }
                    }
                }
            }
        }
        return toReturn;
    }

    private int calculateNumberOfOutputs (final NAryFunction function) {
        for (final Method m : function.getClass().getMethods()) {
            if (m.getName().equals("apply")) {
                final Class returnType = m.getReturnType();
                if (Unit.class.isAssignableFrom(returnType)) {
                    return 0;
                } else if (Tupel.class.isAssignableFrom(returnType)){
                    if (Pair.class.isAssignableFrom(returnType)) {
                        return 2;
                    } else if (Triplet.class.isAssignableFrom(returnType)) {
                        return 3;
                    } else if (Quad.class.isAssignableFrom(returnType)) {
                        return 4;
                    } else if (Quint.class.isAssignableFrom(returnType)) {
                        return 5;
                    } else if (Octet.class.isAssignableFrom (returnType)) {
                        return 8;
                    }
                } else {
                    return 1;
                }
            } 
        }
        return 0;
    }

    private int calculateNumberOfInputs (final NAryFunction function) {
        if (function instanceof ConstantFunction) {
            return 0;
        } else if (function instanceof UnaryFunction) {
            return 1;
        } else if (function instanceof BinaryFunction) {
            return 2;
        } else if (function instanceof TernaryFunction) {
            return  3;
        } else if (function instanceof QuadtaryFunction) {
            return 4;
        } else if (function instanceof QuintaryFunction) {
            return 5;
        } else if (function instanceof OctetaryFunction) {
            return 8;
        } else {
            return -1;
        }
    }

    @SuppressWarnings("unchecked")
    private Object call () {
        final Object [] inputObjects = new Object [inputs];
        final Converter [] converters = new Converter[inputs];
        final ConverterRegistry cregistry = ConverterRegistry.getConverterRegistry();

        for (int i = 0; i < inputs; ++i) {
            inputObjects[i] = connections[i].getResult();
            
            if (inputObjects[i] == null) {
                this.visualisation.setError(true);
                setChanged();
                notifyObservers(false);
                throw new CantGetResultException (function.toString() + " can't get an result from input " + i + ", is the input and all it's predecessors properly connected?");
            }
            if (inputTypes[i].isAssignableFrom(inputObjects[i].getClass())) {
                converters[i] = new IdentityConverter();
            } else {
                converters[i] = cregistry.getConverter(new Pair<Class, Class> (inputObjects[i].getClass (), inputTypes[i]));
            }

            if (converters[i] == null) {
                this.visualisation.setError(true);
                setChanged();
                notifyObservers(false);
                throw new ConversionException ("Can't find converter from " + inputObjects[i].getClass() + " to " + inputTypes[i]);
            }
        }

        for (int i = 0, n = inputObjects.length; i < n; ++i) {
            try {
                Object tmp = converters[i].apply(inputObjects[i]);

                if (tmp == null) { 
                    this.visualisation.setError(true);
                    setChanged();
                    notifyObservers(false);
                    throw new ConversionException ("Can't convert from " + inputObjects[i] + " to " + inputTypes [i] + " on input " + i);
                }
                inputObjects[i] = tmp;

            } catch (Exception e) {
                this.visualisation.setError(true);
                setChanged();
                notifyObservers(false);
                e.printStackTrace();
                throw new ConversionException ("Can't convert from " + inputObjects[i] + " to " + inputTypes [i] + " on input " + i);
            }
        }


        if (visible && animation != null && ((Boolean)gpr.getProperty("animationStatus")) && ! doneOnce) {
            this.visualisation.setHighlighted(true);
            setChanged();
            notifyObservers(true);
            for (int i = 0; i < inputObjects.length; ++i) {
                animation.setOperand(i, inputObjects[i]);
            }
            animation.start();
            doneOnce = true;            
            this.visualisation.setHighlighted(false);
            setChanged();
            notifyObservers(false);
        }


        switch (inputs) {
            case 0 : 
                return ((ConstantFunction)this.function).apply();
            case 1 : 
                return  ((UnaryFunction)this.function).apply(inputObjects[0]); 
            case 2 : 
                return ((BinaryFunction)this.function).apply(inputObjects[0], inputObjects[1]); 
            case 3 : 
                return ((TernaryFunction)this.function).apply(inputObjects[0], inputObjects[1], inputObjects[2]); 
            case 4 : 
                return ((QuadtaryFunction)this.function).apply(inputObjects[0], inputObjects[1],  inputObjects[2], inputObjects[3]);
            case 5 : 
                return ((QuintaryFunction)this.function).apply(inputObjects[0], inputObjects[1],  inputObjects[2], inputObjects[3], inputObjects[4]);
            case 8 : 
                return ((OctetaryFunction)this.function).apply(inputObjects[0], inputObjects[1],  inputObjects[2], inputObjects[3], inputObjects[4], inputObjects[5],
                        inputObjects[6],inputObjects[7]);
        }
        return null;
    }
    
    @Override
    public void reset () {
        super.reset ();
        doneOnce = false;
    }

    public Object apply (final int slot) {
        if (slot > outputs) {
            return null;
        }

        result = call();
        
        switch (outputs) {
            case 0 : return null;
            case 1 : return result;
            case 2 : 
                final Pair p = ((Pair)result);
                switch (slot) {
                    case 0: return p.getFirst();
                    case 1: return p.getSecond();
                    default : return null;
                }

            case 3 : 
                final Triplet t = ((Triplet)result);
                switch (slot) {
                    case 0 : return t.getFirst ();
                    case 1 : return t.getSecond ();
                    case 2 : return t.getThird ();
                    default : return null;
                }
            case 4: 
                final Quad qu = ((Quad)result);
                switch (slot) {
                    case 0 : return qu.getFirst();
                    case 1 : return qu.getSecond();
                    case 2 : return qu.getThird();
                    case 3 : return qu.getForth();
                    default : return null;
                }
            case 5: 
                final Quint qi = ((Quint)result);
                switch (slot) {
                    case 0 : return qi.getFirst();
                    case 1 : return qi.getSecond();
                    case 2 : return qi.getThird();
                    case 3 : return qi.getForth();
                    case 4 : return qi.getFifth();
                    default : return null;
                }
            case 8 : 
                final Octet oc = ((Octet)result);
                switch (slot) {
                    case 0 : return oc.getFirst();
                    case 1 : return oc.getSecond();
                    case 2 : return oc.getThird();
                    case 3 : return oc.getForth();
                    case 4 : return oc.getFifth();
                    case 5 : return oc.getSixth();
                    case 6 : return oc.getSeventh();
                    case 7 : return oc.getEighth();
                    default : return null;
                }
            default : return null;
        }
    }

    public String toString() {
        return function.toString();
    }

    @Override
    public String getDoubleClickActionName() {
        return "Nothing";
    }

    @Override
    public void doubleClick(final DrawingArea dv) {
        JOptionPane.showMessageDialog(dv, "Nothing to be done here go away", "Go away", JOptionPane.ERROR_MESSAGE);
    }

    @Override
    public Element writeYourself () {
        final Element toReturn = super.writeYourself();

        if (animation != null) {
            final Element animationElement = new Element("animation");
            animationElement.setAttribute("type", animation.getClass().getCanonicalName());
            toReturn.addContent(animationElement);
        }
        if (function != null) {
            final Element functionElement = new Element ("function");
            functionElement.setAttribute("type", function.getClass().getCanonicalName());
            toReturn.addContent(functionElement);
        }

        return toReturn;
    }
}
